package com.flca.frw.config;

import java.io.File;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalINIConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.SystemConfiguration;
import org.apache.commons.configuration.XMLConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.flca.frw.util.FileUtils;
import com.flca.frw.util.ShellUtils;

/**
 * this is the base class to maintain Configuration files. Configuration files
 * can be: .ini, .properties or .xml file(s) Given the input via the
 * constructor, the config files are scanned under the classpath, filesystem or
 * both. Config files found under the filesystem are more specific than files
 * under the classpath, so if the same value is found under the classpath and
 * filesystem, than the latter is used. The input classpath and/or filesystem
 * paths, are more specific from top to bottom. The idea is that you have for
 * examples config files under "/config/common" which can be overwritten on a
 * target machine by config files under "/config/server001". You may use ssytem
 * variables like for ex: "/config/$HOSTNAME"
 * 
 * @author robin
 * 
 */
public abstract class ConfigBase implements Config {

	private List<String> mConfigFSDirs;
	private List<String> mConfigCPDirs;
	private List<String> mConfigScanJars;
	private CompositeConfiguration mConfiguration;
	private Map<String, List<FileIdxAndValue>> mLoadedProperties = new HashMap<String, List<FileIdxAndValue>>();
	private List<String> mLoadedFiles = new ArrayList<String>();

	private static final String[] CONFIG_FILE_EXTS = new String[] { "*.ini", "*.xml", "*.properties", };
	private final static FilePatternMatcher sFileMatcher = makeFilePatternMatcher(CONFIG_FILE_EXTS);

	private static Logger logger = LoggerFactory.getLogger(ConfigBase.class);

	/**
	 * ctor for the ConfigBase
	 * 
	 * @param aContextRootDir
	 *            This the path under
	 * @param aConfigFsDirs
	 * @param aConfigCpDirs
	 * @param aConfigScanJars
	 */
	public ConfigBase(List<String> aConfigFsDirs, List<String> aConfigCpDirs, List<String> aConfigScanJars) {
		mConfigFSDirs = aConfigFsDirs;
		mConfigCPDirs = aConfigCpDirs;
		mConfigScanJars = aConfigScanJars;

		initialize();
	}

	@Override
	public List<String> getConfigFSDirs() {
		return mConfigFSDirs;
	}

	@Override
	public List<String> getConfigCPDirs() {
		return mConfigCPDirs;
	}

	@Override
	public List<String> getConfigScanJars() {
		return mConfigScanJars;
	}
	
	

	// -----------------------------------------------------------------------------------------------------
	/*
	 * (non-Javadoc)
	 * 
	 * @see flca.frw.config.ConfigsHelper#getFile(java.lang.String)
	 */
	@Override
	public File getFile(String aFilename) {
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see flca.frw.config.ConfigsHelper#getFileContext(java.lang.String)
	 */
	@Override
	public String getFileContent(String aFilename) {
		return "TODO";
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see flca.frw.config.ConfigsHelper#getAllPropertiesInfo()
	 */
	@Override
	public List<PropertyInfo> getAllPropertiesInfo() {
		List<PropertyInfo> result = new ArrayList<PropertyInfo>();

		for (String propkey : mLoadedProperties.keySet()) {
			PropertyInfo propinfo = new PropertyInfo(propkey, getString(propkey));

			List<FileIdxAndValue> filevalues = mLoadedProperties.get(propkey);
			if (filevalues.size() > 1) {
				for (FileIdxAndValue fileIdxAndValue : filevalues) {
					propinfo.add(new File(mLoadedFiles.get(fileIdxAndValue.idex)), fileIdxAndValue.value);
				}
			}

			result.add(propinfo);
		}

		return result;
	}

	// -------- Configuration interface methodss ----------

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#addProperty(java.lang.
	 * String, java.lang.Object)
	 */
	@Override
	public void addProperty(String key, Object arg1) {
		mConfiguration.addProperty(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.configuration.Configuration#clear()
	 */
	@Override
	public void clear() {
		mConfiguration.clear();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#clearProperty(java.lang
	 * .String)
	 */
	@Override
	public void clearProperty(String key) {
		mConfiguration.clearProperty(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#containsKey(java.lang.
	 * String)
	 */
	@Override
	public boolean containsKey(String key) {
		return mConfiguration.containsKey(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getBigDecimal(java.lang
	 * .String, java.math.BigDecimal)
	 */
	@Override
	public BigDecimal getBigDecimal(String key, BigDecimal arg1) {
		return mConfiguration.getBigDecimal(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getBigDecimal(java.lang
	 * .String)
	 */
	@Override
	public BigDecimal getBigDecimal(String key) {
		return mConfiguration.getBigDecimal(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getBigInteger(java.lang
	 * .String, java.math.BigInteger)
	 */
	@Override
	public BigInteger getBigInteger(String key, BigInteger arg1) {
		return mConfiguration.getBigInteger(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getBigInteger(java.lang
	 * .String)
	 */
	@Override
	public BigInteger getBigInteger(String key) {
		return mConfiguration.getBigInteger(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getBoolean(java.lang.String
	 * , boolean)
	 */
	@Override
	public boolean getBoolean(String key, boolean arg1) {
		return mConfiguration.getBoolean(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getBoolean(java.lang.String
	 * , java.lang.Boolean)
	 */
	@Override
	public Boolean getBoolean(String key, Boolean arg1) {
		return mConfiguration.getBoolean(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getBoolean(java.lang.String
	 * )
	 */
	@Override
	public boolean getBoolean(String key) {
		return mConfiguration.getBoolean(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getByte(java.lang.String,
	 * byte)
	 */
	@Override
	public byte getByte(String key, byte arg1) {
		return mConfiguration.getByte(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getByte(java.lang.String,
	 * java.lang.Byte)
	 */
	@Override
	public Byte getByte(String key, Byte arg1) {
		return mConfiguration.getByte(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getByte(java.lang.String)
	 */
	@Override
	public byte getByte(String key) {
		return mConfiguration.getByte(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getDouble(java.lang.String
	 * , double)
	 */
	@Override
	public double getDouble(String key, double arg1) {
		return mConfiguration.getDouble(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getDouble(java.lang.String
	 * , java.lang.Double)
	 */
	@Override
	public Double getDouble(String key, Double arg1) {
		return mConfiguration.getDouble(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getDouble(java.lang.String
	 * )
	 */
	@Override
	public double getDouble(String key) {
		return mConfiguration.getDouble(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getFloat(java.lang.String,
	 * float)
	 */
	@Override
	public float getFloat(String key, float arg1) {
		return mConfiguration.getFloat(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getFloat(java.lang.String,
	 * java.lang.Float)
	 */
	@Override
	public Float getFloat(String key, Float arg1) {
		return mConfiguration.getFloat(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getFloat(java.lang.String)
	 */
	@Override
	public float getFloat(String key) {
		return mConfiguration.getFloat(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getInt(java.lang.String,
	 * int)
	 */
	@Override
	public int getInt(String key, int arg1) {
		return mConfiguration.getInt(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getInt(java.lang.String)
	 */
	@Override
	public int getInt(String key) {
		return mConfiguration.getInt(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getInteger(java.lang.String
	 * , java.lang.Integer)
	 */
	@Override
	public Integer getInteger(String key, Integer arg1) {
		return mConfiguration.getInteger(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.configuration.Configuration#getKeys()
	 */
	@Override
	public Iterator<String> getKeys() {
		return mConfiguration.getKeys();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getKeys(java.lang.String)
	 */
	@Override
	public Iterator<String> getKeys(String key) {
		return mConfiguration.getKeys(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getList(java.lang.String)
	 */
	@Override
	public List<Object> getList(String key) {
		return mConfiguration.getList(key);
	}

	@SuppressWarnings("rawtypes")
	@Override
	public List<Object> getList(String arg0, List arg1) {
		return mConfiguration.getList(arg0, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getLong(java.lang.String,
	 * long)
	 */
	@Override
	public long getLong(String key, long arg1) {
		return mConfiguration.getLong(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getLong(java.lang.String,
	 * java.lang.Long)
	 */
	@Override
	public Long getLong(String key, Long arg1) {
		return mConfiguration.getLong(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getLong(java.lang.String)
	 */
	@Override
	public long getLong(String key) {
		return mConfiguration.getLong(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getProperties(java.lang
	 * .String)
	 */
	@Override
	public Properties getProperties(String key) {
		return mConfiguration.getProperties(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getProperty(java.lang.
	 * String)
	 */
	@Override
	public Object getProperty(String key) {
		return mConfiguration.getProperty(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getShort(java.lang.String,
	 * short)
	 */
	@Override
	public short getShort(String key, short arg1) {
		return mConfiguration.getShort(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getShort(java.lang.String,
	 * java.lang.Short)
	 */
	@Override
	public Short getShort(String key, Short arg1) {
		return mConfiguration.getShort(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getShort(java.lang.String)
	 */
	@Override
	public short getShort(String key) {
		return mConfiguration.getShort(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getString(java.lang.String
	 * , java.lang.String)
	 */
	@Override
	public String getString(String key, String arg1) {
		return mConfiguration.getString(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getString(java.lang.String
	 * )
	 */
	@Override
	public String getString(String key) {
		return mConfiguration.getString(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#getStringArray(java.lang
	 * .String)
	 */
	@Override
	public String[] getStringArray(String key) {
		return mConfiguration.getStringArray(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.commons.configuration.Configuration#isEmpty()
	 */
	@Override
	public boolean isEmpty() {
		return mConfiguration.isEmpty();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#setProperty(java.lang.
	 * String, java.lang.Object)
	 */
	@Override
	public void setProperty(String key, Object arg1) {
		mConfiguration.setProperty(key, arg1);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.apache.commons.configuration.Configuration#subset(java.lang.String)
	 */
	@Override
	public Configuration subset(String key) {
		return mConfiguration.subset(key);
	}

	// ------------ private functions -------------------------------

	/**
	 * This is method run during startup that will scan for all files in the
	 * given path's
	 */
	private void initialize() {
		mConfiguration = new CompositeConfiguration();
		mConfiguration.addConfiguration(new SystemConfiguration()); // the most
																	// specific!

		initFromExternalFiles();
		initFromZipEntries(); // config files inside jar files are the least
								// specific hence we do this last
	}

	private void initFromExternalFiles() {
		for (String file : findAllFiles()) {
			try {
				AbstractConfiguration config = configFactory(file);
				Iterator<String> keys = config.getKeys();
				while (keys.hasNext()) {
					processFile(file, config, keys);
				}
				mConfiguration.addConfiguration(config);
			} catch (ConfigurationException e) {
				logger.error("error in initFromExternalFiles", e);
			}
		}
	}

	private void initFromZipEntries() {
		if (mConfigScanJars != null && mConfigScanJars.size() > 0) {
			findAllZipEntries();
		}
	}

	private void processFile(String file, AbstractConfiguration config, Iterator<String> keys) {
		String key = keys.next();
		String value = config.getString(key);
		if (!mLoadedProperties.containsKey(key)) {
			mLoadedProperties.put(key, new ArrayList<FileIdxAndValue>());
		}
		if (!mLoadedFiles.contains(file)) {
			mLoadedFiles.add(file);
		}
		int idx = mLoadedFiles.indexOf(file);
		mLoadedProperties.get(key).add(new FileIdxAndValue(idx, value));
	}

	private void findAllZipEntries() {
		for (String jarfile : findClassPathJars()) {
			processZipFile(jarfile);
		}
	}

	private void processZipFile(String jarfile) {
		ZipFile zipfile = null;
		try {
			zipfile = new ZipFile(jarfile);
			Enumeration<?> entries = zipfile.entries();
			if (entries.hasMoreElements()) {
				ZipEntry entry = (ZipEntry) entries.nextElement();
				if (sFileMatcher.matches(entry.getName())) {
					mConfiguration.addConfiguration(configFactory(zipfile, entry));
				}
			}
		} catch (Exception e) {
			logger.error("error in findAllZipEntries while processing " + jarfile, e);
		} finally {
			if (zipfile != null) {
				try {zipfile.close();} catch (Exception e) {}
			}
		}
	}

	private AbstractConfiguration configFactory(String aFilename) throws ConfigurationException {
		if (aFilename.toLowerCase().endsWith(".ini")) {
			return new HierarchicalINIConfiguration(aFilename);
		} else if (aFilename.toLowerCase().endsWith(".xml")) {
			return new XMLConfiguration(aFilename);
		} else if (aFilename.toLowerCase().endsWith(".properties")) {
			return new PropertiesConfiguration(aFilename);
		} else {
			logger.error("configFactory found filename with wrond extendsion : " + aFilename);
			return null; // this should be impossible!
		}
	}

	private AbstractConfiguration configFactory(ZipFile aZipFile, ZipEntry aZipEntry) {
		InputStream is = null;
		try {
			is = aZipFile.getInputStream(aZipEntry);
			if (aZipEntry.getName().toLowerCase().endsWith(".ini")) {
				HierarchicalINIConfiguration config = new HierarchicalINIConfiguration();
				config.load(is);
				return config;
			} else if (aZipEntry.getName().toLowerCase().endsWith(".xml")) {
				XMLConfiguration config = new XMLConfiguration();
				config.load(is);
				return config;
			} else if (aZipEntry.getName().toLowerCase().endsWith(".properties")) {
				PropertiesConfiguration config = new PropertiesConfiguration();
				config.load(is);
				return config;
			} else {
				logger.error("ZipEntry found with wrong extension in : " + aZipFile + " " + aZipEntry);
				return null;
			}
		} catch (Exception ex) {
			logger.error("error in configFactory while processing " + aZipFile + " " + aZipEntry);
			return null;
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (Exception ex) {
				}
			}
		}

	}

	private List<String> findAllFiles() {
		List<String> result = findFileSystemFiles(); // these are more specific
														// then classpath files,
														// hence we do these
														// first
		result.addAll(findClassPathFiles());
		return result;
	}

	private List<String> findFileSystemFiles() {
		List<String> result = new ArrayList<String>();

		if (getConfigFSDirs() != null && getConfigFSDirs().size() > 0) {
			for (String folder : getConfigFSDirs()) {
				result.addAll(new ConfigFilesList(new File(folder), sFileMatcher).getFiles());
			}
		}

		return result;
	}

	private List<String> findClassPathFiles() {
		List<String> result = new ArrayList<String>();

		if (getConfigCPDirs() != null && getConfigCPDirs().size() > 0) {
			for (String folder : getConfigCPDirs()) {
				result.addAll(new ConfigFilesList(new File(folder), sFileMatcher).getResources());
			}
		}

		return result;
	}

	final String classPath = System.getProperty("java.class.path", ".");
	final String[] classPathElements = classPath.split(getClassPathDelim());

	private List<String> findClassPathJars() {
		List<String> result = new ArrayList<String>();

		for (String cpElem : classPathElements) {
			File f = new File(cpElem);
			if (f.isDirectory()) {
				List<String> jars = FileUtils.findFiles(cpElem, new FilePatternFilter("*.jar:"), true);
				for (String jar : jars) {
					for (String scanjar : mConfigScanJars) {
						FileNameFilter fpm = new FileNameFilter(scanjar);
						if (fpm.accept(new File(jar))) {
							result.add(jar);
						}
					}
				}
			}
		}
		return result;
	}

	private static FilePatternMatcher makeFilePatternMatcher(String aWildcards[]) {
		String lookfor = "";
		for (String ext : aWildcards) {
			lookfor += ext + ShellUtils.getClassPathDelim();
		}
		FilePatternMatcher pattern = new FilePatternMatcher(lookfor);
		return pattern;
	}

	private String getClassPathDelim() {
		return (ShellUtils.isLinux()) ? ":" : ";";
	}

	// -------------------
	class FileIdxAndValue {
		int idex;
		String value;

		public FileIdxAndValue(int idex, String value) {
			super();
			this.idex = idex;
			this.value = value;
		}
	}

}